﻿using System;
using System.Globalization;
using System.Runtime.InteropServices;

namespace Nomo.CoreModule
{
    /// <summary>
    /// 
    /// </summary>
    [StructLayout(LayoutKind.Explicit)]
    public readonly struct PrimitiveValue : IEquatable<PrimitiveValue>, IConvertible, IComparable<PrimitiveValue>
    {
        #region field

        [FieldOffset(0)] private readonly TypeCode _typeCode;
        [FieldOffset(4)] private readonly bool     _boolValue;
        [FieldOffset(4)] private readonly char     _charValue;
        [FieldOffset(4)] private readonly byte     _byteValue;
        [FieldOffset(4)] private readonly sbyte    _sbyteValue;
        [FieldOffset(4)] private readonly short    _shortValue;
        [FieldOffset(4)] private readonly ushort   _ushortValue;
        [FieldOffset(4)] private readonly int      _intValue;
        [FieldOffset(4)] private readonly uint     _uintValue;
        [FieldOffset(4)] private readonly long     _longValue;
        [FieldOffset(4)] private readonly ulong    _ulongValue;
        [FieldOffset(4)] private readonly float    _floatValue;
        [FieldOffset(4)] private readonly double   _doubleValue;

        #endregion

        #region property

        /// <summary>
        /// 
        /// </summary>
        public bool IsEmpty => _typeCode == TypeCode.Empty;

        #endregion

        #region constructor

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(bool value) : this()
        {
            _typeCode  = TypeCode.Boolean;
            _boolValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(char value) : this()
        {
            _typeCode  = TypeCode.Char;
            _charValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(byte value) : this()
        {
            _typeCode  = TypeCode.Byte;
            _byteValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(sbyte value) : this()
        {
            _typeCode   = TypeCode.SByte;
            _sbyteValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(short value) : this()
        {
            _typeCode   = TypeCode.Int16;
            _shortValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(ushort value) : this()
        {
            _typeCode    = TypeCode.UInt16;
            _ushortValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(int value) : this()
        {
            _typeCode = TypeCode.Int32;
            _intValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(uint value) : this()
        {
            _typeCode  = TypeCode.UInt32;
            _uintValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(long value) : this()
        {
            _typeCode  = TypeCode.Int64;
            _longValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(ulong value) : this()
        {
            _typeCode   = TypeCode.UInt64;
            _ulongValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(float value) : this()
        {
            _typeCode   = TypeCode.Single;
            _floatValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public PrimitiveValue(double value) : this()
        {
            _typeCode    = TypeCode.Double;
            _doubleValue = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <exception cref="NotSupportedException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        public PrimitiveValue(object value) : this()
        {
            if (value == null)
            {
                return;
            }

            switch (value)
            {
                case bool boolValue:
                    _typeCode  = TypeCode.Boolean;
                    _boolValue = boolValue;
                    return;
                case char charValue:
                    _typeCode  = TypeCode.Char;
                    _charValue = charValue;
                    return;
                case byte byteValue:
                    _typeCode  = TypeCode.Byte;
                    _byteValue = byteValue;
                    return;
                case sbyte sbyteValue:
                    _typeCode   = TypeCode.SByte;
                    _sbyteValue = sbyteValue;
                    return;
                case short shortValue:
                    _typeCode   = TypeCode.Int16;
                    _shortValue = shortValue;
                    return;
                case ushort ushortValue:
                    _typeCode    = TypeCode.UInt16;
                    _ushortValue = ushortValue;
                    return;
                case int intValue:
                    _typeCode = TypeCode.Int32;
                    _intValue = intValue;
                    return;
                case uint uintValue:
                    _typeCode  = TypeCode.UInt32;
                    _uintValue = uintValue;
                    return;
                case long longValue:
                    _typeCode  = TypeCode.Int64;
                    _longValue = longValue;
                    return;
                case ulong ulongValue:
                    _typeCode   = TypeCode.UInt64;
                    _ulongValue = ulongValue;
                    return;
                case float floatValue:
                    _typeCode   = TypeCode.Single;
                    _floatValue = floatValue;
                    return;
                case double doubleValue:
                    _typeCode    = TypeCode.Double;
                    _doubleValue = doubleValue;
                    return;
                case Enum enumValue:
                    var type     = enumValue.GetType().GetEnumUnderlyingType();
                    var typeCode = Type.GetTypeCode(type);
                    switch (typeCode)
                    {
                        case TypeCode.SByte:
                            _typeCode   = TypeCode.SByte;
                            _sbyteValue = Convert.ToSByte(enumValue);
                            break;
                        case TypeCode.Byte:
                            _typeCode  = TypeCode.Byte;
                            _byteValue = Convert.ToByte(enumValue);
                            break;
                        case TypeCode.Int16:
                            _typeCode   = TypeCode.Int16;
                            _shortValue = Convert.ToInt16(enumValue);
                            break;
                        case TypeCode.UInt16:
                            _typeCode    = TypeCode.UInt16;
                            _ushortValue = Convert.ToUInt16(enumValue);
                            break;
                        case TypeCode.Int32:
                            _typeCode = TypeCode.Int32;
                            _intValue = Convert.ToInt32(enumValue);
                            break;
                        case TypeCode.UInt32:
                            _typeCode  = TypeCode.UInt32;
                            _uintValue = Convert.ToUInt32(enumValue);
                            break;
                        case TypeCode.Int64:
                            _typeCode  = TypeCode.Int64;
                            _longValue = Convert.ToInt64(enumValue);
                            break;
                        case TypeCode.UInt64:
                            _typeCode   = TypeCode.UInt64;
                            _ulongValue = Convert.ToUInt64(enumValue);
                            break;
                        case TypeCode.Empty:
                        case TypeCode.Object:
                        case TypeCode.DBNull:
                        case TypeCode.Boolean:
                        case TypeCode.Char:
                        case TypeCode.Single:
                        case TypeCode.Double:
                        case TypeCode.Decimal:
                        case TypeCode.DateTime:
                        case TypeCode.String:
                            throw new NotSupportedException();
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                    return;
            }

            throw new NotSupportedException();
        }

        #endregion

        /// <inheritdoc />
        public override bool Equals(object obj)
        {
            return obj is PrimitiveValue primitiveValue && Equals(primitiveValue);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static bool operator ==(PrimitiveValue a, PrimitiveValue b)
        {
            return a._typeCode == b._typeCode && a._doubleValue == b._doubleValue;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static bool operator !=(PrimitiveValue a, PrimitiveValue b)
        {
            return a._typeCode != b._typeCode || a._doubleValue != b._doubleValue;
        }

        /// <inheritdoc />
        public override unsafe int GetHashCode()
        {
            unchecked
            {
                fixed (double* pointer = &_doubleValue)
                {
                    var hashCode = _typeCode.GetHashCode();
                    hashCode = (hashCode * 397) ^ pointer->GetHashCode();
                    return hashCode;
                }
            }
        }

        /// <inheritdoc />
        public override string ToString()
        {
            switch (_typeCode)
            {
                case TypeCode.Empty:
                    return string.Empty;
                case TypeCode.Boolean:
                    return _boolValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.Char:
                    return _charValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.SByte:
                    return _sbyteValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.Byte:
                    return _byteValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.Int16:
                    return _shortValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.UInt16:
                    return _ushortValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.Int32:
                    return _intValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.UInt32:
                    return _uintValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.Int64:
                    return _longValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.UInt64:
                    return _ulongValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.Single:
                    return _floatValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.Double:
                    return _doubleValue.ToString(NumberFormatInfo.InvariantInfo);
                case TypeCode.Object:
                case TypeCode.DBNull:
                case TypeCode.Decimal:
                case TypeCode.DateTime:
                case TypeCode.String:
                    throw new InvalidOperationException();
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(bool value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(char value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(byte value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(sbyte value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(short value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(ushort value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(int value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(uint value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(long value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(ulong value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(float value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator PrimitiveValue(double value)
        {
            return new PrimitiveValue(value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator bool(PrimitiveValue value)
        {
            return value.ToBoolean();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator char(PrimitiveValue value)
        {
            return value.ToChar();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator byte(PrimitiveValue value)
        {
            return value.ToByte();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator sbyte(PrimitiveValue value)
        {
            return value.ToSByte();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator short(PrimitiveValue value)
        {
            return value.ToInt16();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator ushort(PrimitiveValue value)
        {
            return value.ToUInt16();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator int(PrimitiveValue value)
        {
            return value.ToInt32();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator uint(PrimitiveValue value)
        {
            return value.ToUInt32();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator long(PrimitiveValue value)
        {
            return value.ToInt64();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator ulong(PrimitiveValue value)
        {
            return value.ToUInt64();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator float(PrimitiveValue value)
        {
            return value.ToSingle();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static implicit operator double(PrimitiveValue value)
        {
            return value.ToDouble();
        }

        #region IEquatable<> member

        /// <inheritdoc />
        public bool Equals(PrimitiveValue other)
        {
            return this == other;
        }

        #endregion

        #region IConvertible member

        /// <inheritdoc />
        public TypeCode GetTypeCode()
        {
            return _typeCode;
        }

        /// <inheritdoc />
        public bool ToBoolean(IFormatProvider provider = null)
        {
            switch (_typeCode)
            {
                case TypeCode.Empty:
                    return default;
                case TypeCode.Boolean:
                    return _boolValue;
                case TypeCode.Char:
                    return _charValue != default;
                case TypeCode.SByte:
                    return _sbyteValue != default;
                case TypeCode.Byte:
                    return _byteValue != default;
                case TypeCode.Int16:
                    return _shortValue != default;
                case TypeCode.UInt16:
                    return _ushortValue != default;
                case TypeCode.Int32:
                    return _intValue != default;
                case TypeCode.UInt32:
                    return _uintValue != default;
                case TypeCode.Int64:
                    return _longValue != default;
                case TypeCode.UInt64:
                    return _ulongValue != default;
                case TypeCode.Single:
                    return _floatValue != default;
                case TypeCode.Double:
                    return _doubleValue != default;
                case TypeCode.Object:
                case TypeCode.DBNull:
                case TypeCode.Decimal:
                case TypeCode.DateTime:
                case TypeCode.String:
                    throw new InvalidOperationException();
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <inheritdoc />
        public char ToChar(IFormatProvider provider = null)
        {
            switch (_typeCode)
            {
                case TypeCode.Empty:
                case TypeCode.Boolean:
                case TypeCode.Single:
                case TypeCode.Double:
                    return default;
                case TypeCode.Char:
                    return _charValue;
                case TypeCode.SByte:
                case TypeCode.Byte:
                case TypeCode.Int16:
                case TypeCode.UInt16:
                case TypeCode.Int32:
                case TypeCode.UInt32:
                case TypeCode.Int64:
                case TypeCode.UInt64:
                    return (char) ToInt64(provider);
                case TypeCode.Object:
                case TypeCode.DBNull:
                case TypeCode.Decimal:
                case TypeCode.DateTime:
                case TypeCode.String:
                    throw new InvalidOperationException();
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <inheritdoc />
        public sbyte ToSByte(IFormatProvider provider = null)
        {
            return (sbyte) ToInt64();
        }

        /// <inheritdoc />
        public byte ToByte(IFormatProvider provider = null)
        {
            return (byte) ToInt64();
        }

        /// <inheritdoc />
        public short ToInt16(IFormatProvider provider = null)
        {
            return (short) ToInt64();
        }

        /// <inheritdoc />
        public ushort ToUInt16(IFormatProvider provider = null)
        {
            return (ushort) ToUInt64();
        }

        /// <inheritdoc />
        public int ToInt32(IFormatProvider provider = null)
        {
            return (int) ToInt64();
        }

        /// <inheritdoc />
        public uint ToUInt32(IFormatProvider provider = null)
        {
            return (uint) ToUInt64();
        }

        /// <inheritdoc />
        public long ToInt64(IFormatProvider provider = null)
        {
            switch (_typeCode)
            {
                case TypeCode.Empty:
                    return default;
                case TypeCode.Boolean:
                    return !_boolValue ? 0L : 1L;
                case TypeCode.Char:
                    return _charValue;
                case TypeCode.SByte:
                    return _sbyteValue;
                case TypeCode.Byte:
                    return _byteValue;
                case TypeCode.Int16:
                    return _shortValue;
                case TypeCode.UInt16:
                    return _ushortValue;
                case TypeCode.Int32:
                    return _intValue;
                case TypeCode.UInt32:
                    return _uintValue;
                case TypeCode.Int64:
                    return _longValue;
                case TypeCode.UInt64:
                    return (long) _ulongValue;
                case TypeCode.Single:
                    return (long) _floatValue;
                case TypeCode.Double:
                    return (long) _doubleValue;
                case TypeCode.Object:
                case TypeCode.DBNull:
                case TypeCode.Decimal:
                case TypeCode.DateTime:
                case TypeCode.String:
                    throw new InvalidOperationException();
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <inheritdoc />
        public ulong ToUInt64(IFormatProvider provider = null)
        {
            switch (_typeCode)
            {
                case TypeCode.Empty:
                    return default;
                case TypeCode.Boolean:
                    return !_boolValue ? 0UL : 1UL;
                case TypeCode.Char:
                    return _charValue;
                case TypeCode.SByte:
                    return (ulong) _sbyteValue;
                case TypeCode.Byte:
                    return _byteValue;
                case TypeCode.Int16:
                    return (ulong) _shortValue;
                case TypeCode.UInt16:
                    return _ushortValue;
                case TypeCode.Int32:
                    return (ulong) _intValue;
                case TypeCode.UInt32:
                    return _uintValue;
                case TypeCode.Int64:
                    return (ulong) _longValue;
                case TypeCode.UInt64:
                    return _ulongValue;
                case TypeCode.Single:
                    return (ulong) _floatValue;
                case TypeCode.Double:
                    return (ulong) _doubleValue;
                case TypeCode.Object:
                case TypeCode.DBNull:
                case TypeCode.Decimal:
                case TypeCode.DateTime:
                case TypeCode.String:
                    throw new InvalidOperationException();
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <inheritdoc />
        public float ToSingle(IFormatProvider provider = null)
        {
            return (float) ToDouble();
        }

        /// <inheritdoc />
        public double ToDouble(IFormatProvider provider = null)
        {
            switch (_typeCode)
            {
                case TypeCode.Empty:
                    return default;
                case TypeCode.Boolean:
                    return !_boolValue ? 0.0 : 1.0;
                case TypeCode.Char:
                    return _charValue;
                case TypeCode.SByte:
                    return _sbyteValue;
                case TypeCode.Byte:
                    return _byteValue;
                case TypeCode.Int16:
                    return _shortValue;
                case TypeCode.UInt16:
                    return _ushortValue;
                case TypeCode.Int32:
                    return _intValue;
                case TypeCode.UInt32:
                    return _uintValue;
                case TypeCode.Int64:
                    return _longValue;
                case TypeCode.UInt64:
                    return _ulongValue;
                case TypeCode.Single:
                    return _floatValue;
                case TypeCode.Double:
                    return _doubleValue;
                case TypeCode.Object:
                case TypeCode.DBNull:
                case TypeCode.Decimal:
                case TypeCode.DateTime:
                case TypeCode.String:
                    throw new InvalidOperationException();
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <inheritdoc />
        public decimal ToDecimal(IFormatProvider provider = null)
        {
            return new decimal(ToDouble(provider));
        }

        /// <inheritdoc />
        public DateTime ToDateTime(IFormatProvider provider = null)
        {
            throw new NotSupportedException();
        }

        /// <inheritdoc />
        public string ToString(IFormatProvider provider)
        {
            return ToString();
        }

        /// <inheritdoc />
        public object ToType(Type conversionType, IFormatProvider provider)
        {
            var typeCode = Type.GetTypeCode(conversionType);
            switch (typeCode)
            {
                case TypeCode.Empty:
                    // ReSharper disable AssignNullToNotNullAttribute
                    return default;
                // ReSharper restore AssignNullToNotNullAttribute
                case TypeCode.Boolean:
                    return ToBoolean();
                case TypeCode.Char:
                    return ToChar();
                case TypeCode.SByte:
                    return ToSByte();
                case TypeCode.Byte:
                    return ToByte();
                case TypeCode.Int16:
                    return ToInt16();
                case TypeCode.UInt16:
                    return ToUInt16();
                case TypeCode.Int32:
                    return ToInt32();
                case TypeCode.UInt32:
                    return ToUInt32();
                case TypeCode.Int64:
                    return ToInt64();
                case TypeCode.UInt64:
                    return ToUInt64();
                case TypeCode.Single:
                    return ToSingle();
                case TypeCode.Double:
                    return ToDouble();
                case TypeCode.String:
                case TypeCode.Object:
                    return ToString();
                case TypeCode.Decimal:
                    return ToDecimal();
                case TypeCode.DBNull:
                case TypeCode.DateTime:
                    throw new NotSupportedException();
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        #endregion

        /// <inheritdoc />
        public int CompareTo(PrimitiveValue other)
        {
            return this == other ? 0 : ToDouble().CompareTo(other.ToDouble());
        }
    }
}
